package uicgas.station;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashMap;
import java.util.Map;

import org.apache.commons.lang.builder.ToStringBuilder;
import org.apache.log4j.Logger;

import uicgas.communication.RemoteCommand;
import uicgas.globaldata.ClientTypes;
import uicgas.globaldata.ControlMessage;
import uicgas.globaldata.CreditCard;
import uicgas.globaldata.Messages;
import uicgas.globaldata.SelfAnnouncement;
import uicgas.globaldata.Transaction;
import uicgas.station.remotecmds.ActivatePump;
import uicgas.station.remotecmds.AnnounceAllConnectedPumps;
import uicgas.station.remotecmds.AnnounceDecommPumps;
import uicgas.station.remotecmds.AnnouncePumpState;
import uicgas.station.remotecmds.AnnounceTransaction;
import uicgas.station.remotecmds.ChangePumpState;
import uicgas.station.remotecmds.ChangeRecommPassword;
import uicgas.station.remotecmds.ChargeCreditCard;
import uicgas.station.remotecmds.ConnectPump;
import uicgas.station.remotecmds.ConnectRegister;
import uicgas.station.remotecmds.DeactivatePump;
import uicgas.station.remotecmds.DecommissionPump;
import uicgas.station.remotecmds.DequeueAssistance;
import uicgas.station.remotecmds.NotifyRegisterAssistanceNeeded;
import uicgas.station.remotecmds.RecommissionPump;
import uicgas.station.remotecmds.ValidateCard;
import uicgas.station.remotecmds.ValidateRecommPassword;
import uicgas.tools.Assert;
import uicgas.tools.LogHelper;
import uicgas.tools.UicGasGeneralException;

public class Server_Comm
{
	private static final Logger LOGGER = LogHelper.getLogger(Server_Comm.class);

	private class ClientConnection extends Thread
	{
		private Integer sourceID = null;
		private Socket socket = null;

		private ClientConnection(Integer sourceID, Socket socket)
		{
			this.sourceID = sourceID;
			this.socket = socket;
		}

		@Override
		public String toString()
		{
			return sourceID + " : " + socket;
		}

		@Override
		public void run()
		{
			try
			{

				while (true)
				{
					//START READING AND PARSING COMMANDS
					ObjectInputStream ois = new ObjectInputStream(
							socket.getInputStream());
					ControlMessage inputMessage = (ControlMessage) ois
							.readObject();
					processControlMessage(sourceID, inputMessage);
				}
			}
			catch (Exception e)
			{
				LOGGER.error(LogHelper.LOG_LAYER_ONE
						+ "Disconnected from Client[" + socket + "]. ");
				//				throw new UicGasGeneralException(e);
				removeClient(sourceID);
			}
		}

		private void sendControlMessage(ControlMessage message)
		{
			try
			{
				sendMessageToSocket(message, socket);
			}
			catch (IOException e)
			{
				throw new UicGasGeneralException(e);
			}
		}
	}

	//ServerSocket ;
	//all members necessary for server
	private ServerSocket serverSocket;

	private StationLogic thisStation;
	private int listenPort;

	private Map<Integer, ClientConnection> clientConnectionsByID = null;

	private ClientTypes serverType;

	public Server_Comm(int port, StationLogic station, ClientTypes type)
	{
		listenPort = port;
		thisStation = station;
		serverType = type;

		clientConnectionsByID = new HashMap<Integer, ClientConnection>();
	}

	/**
	 * Open a serverSocket, then listen Socket coming in a individual thread
	 */
	public int startServer()
	{
		try
		{
			serverSocket = new ServerSocket(listenPort);
		}
		catch (IOException e)
		{
			LOGGER.error("Error opening server_socket channel...", e);
			return -1;
		}

		try
		{
			listenAndStoreClientSocketInNewThread();
		}
		catch (UicGasGeneralException e)
		{
			LOGGER.error("Error! ...", e);
			return -1;
		}

		return 0;
	}

	private void listenAndStoreClientSocketInNewThread()
	{
		new Thread()
		{
			@Override
			public void run()
			{
				Socket clientSocket = null;
				Integer availableID = null;
				try
				{
					ObjectInputStream inputStream = null;
					while (true)
					{
						clientSocket = serverSocket.accept();
						inputStream = new ObjectInputStream(
								clientSocket.getInputStream());

						ControlMessage messageFromClient = (ControlMessage) inputStream
								.readObject();
						availableID = processRegisterMessage(clientSocket,
								messageFromClient);

						while (availableID == null)
						{
							inputStream = new ObjectInputStream(
									clientSocket.getInputStream());
							messageFromClient = (ControlMessage) inputStream
									.readObject();
							availableID = processRegisterMessage(clientSocket,
									messageFromClient);
						}

						ClientConnection connection = new ClientConnection(
								availableID, clientSocket);

						clientConnectionsByID.put(availableID, connection);

						connection.start();

						////////////////////////////////////////////////////////
						//						// if max store size reached
						//						if (clientSocketByID.size() >= serverType
						//								.getMaxConnectionNumber())
						//						{
						//							LOGGER.warn("MaxConnectionNumber["
						//									+ serverType.getMaxConnectionNumber()
						//									+ "] for ClientTypes["
						//									+ serverType
						//									+ "] reached, Server will on longer accept connection. ");
						//
						//							break;
						//						}
						////////////////////////////////////////////////////////
					}

				}
				catch (IOException e)
				{

					//					throw new UicGasGeneralException(e);
					removeClient(availableID);
					LOGGER.error(LogHelper.LOG_LAYER_ONE
							+ "Disconnected from Client[" + clientSocket
							+ "]. ");

				}
				catch (ClassNotFoundException e)
				{
					throw new UicGasGeneralException(e);
				}
			}

		}.start();

	}

	private void removeClient(Integer id)
	{
		clientConnectionsByID.remove(id);
		if (serverType == ClientTypes.PUMP)
		{
			thisStation.removePumpFromStation(id);
		}
		else if (serverType == ClientTypes.REGISTER)
		{
			thisStation.removeRegisterFromStation(id);
		}

		if (LOGGER.isDebugEnabled())
		{
			LOGGER.debug(serverType + "[" + id
					+ "] has been removed from Server Station. ");
		}
	}

	/**
	 * Return ID if it is available; otherwise return null
	 * 
	 * @param socket
	 * @param messageFromClient
	 * @return
	 * @throws IOException
	 */
	private Integer processRegisterMessage(Socket socket,
			ControlMessage messageFromClient) throws IOException
	{
		if (serverType.getRegisterMessage() != messageFromClient.getMessage())
		{
			sendMessageToSocket(new ControlMessage(Messages.ERROR, null),
					socket);
			return null;
		}

		SelfAnnouncement clientSelfAnnouncement = (SelfAnnouncement) messageFromClient
				.getPayload();
		Integer clientID = clientSelfAnnouncement.getID();

		if (clientConnectionsByID.containsKey(clientID))
		{
			LOGGER.error("Requested ID [" + clientID + "] already in use. ");
			sendMessageToSocket(new ControlMessage(Messages.ERROR,
					clientSelfAnnouncement), socket);

			return null;
		}

		if (clientConnectionsByID.size() >= serverType.getMaxConnectionNumber())
		{
			LOGGER.error("Error connecting new client: max capacity of ["
					+ serverType.getMaxConnectionNumber() + "]. ");
			sendMessageToSocket(new ControlMessage(Messages.ERROR,
					clientSelfAnnouncement), socket);
			return null;
		}

		sendMessageToSocket(new ControlMessage(Messages.IDOK,
				clientSelfAnnouncement), socket);

		connectComponents(clientSelfAnnouncement);

		return Integer.valueOf(clientID);
	}

	protected void connectComponents(SelfAnnouncement clientSelfAnnouncement)
	{
		if (ClientTypes.PUMP == serverType)
		{
			new ConnectPump(this.thisStation)
					.executeRemoteCommand(clientSelfAnnouncement);
		}
		else if (ClientTypes.REGISTER == serverType)
		{
			new ConnectRegister(this.thisStation)
					.executeRemoteCommand(clientSelfAnnouncement);
		}
		else
		{
			throw new UicGasGeneralException("Unsupported Server Type ["
					+ serverType + "]. ");
		}
	}

	private void sendMessageToSocket(ControlMessage controlMessage,
			Socket socket) throws IOException
	{
		Assert.notNull(controlMessage);
		if (LOGGER.isDebugEnabled())
		{
			LOGGER.debug(LogHelper.OUT_GOING + "Begin to send ControlMessage["
					+ ToStringBuilder.reflectionToString(controlMessage)
					+ "] to " + socket);
		}

		ObjectOutputStream outputStream = new ObjectOutputStream(
				socket.getOutputStream());
		outputStream.writeObject(controlMessage);

		if (LOGGER.isDebugEnabled())
		{
			LOGGER.debug(LogHelper.OUT_GOING + "End to send ControlMessage["
					+ ToStringBuilder.reflectionToString(controlMessage)
					+ "] to " + socket);
		}
	}

	protected void processControlMessage(int sourceID,
			ControlMessage inputMessage)
	{
		Messages command = inputMessage.getMessage();
		if (command == null)
		{
			if (LOGGER.isDebugEnabled())
			{
				LOGGER.debug(LogHelper.IN_COMING + "Recieved message[null]. ");
			}

			return;
		}

		if (LOGGER.isDebugEnabled())
		{
			LOGGER.debug(LogHelper.IN_COMING + "Recieved message[" + command
					+ "]. ");
		}

		RemoteCommand remoteCommand = null;
		switch (command)
		{
			case ACTIVATEPUMP:
				remoteCommand = new ActivatePump(this.thisStation);
				break;
			//output specifics
			//System.out.println();
			case ANNOUNCEALLCONNECTEDPUMPS:
				remoteCommand = new AnnounceAllConnectedPumps(this.thisStation);
				break;
			case ANNOUNCEDECOMMPUMPS:
				remoteCommand = new AnnounceDecommPumps(this.thisStation);
				break;
			case ANNOUNCEPUMPSTATE:
				remoteCommand = new AnnouncePumpState(this.thisStation);
				break;
			case ANNOUNCETRANSACTION:
				remoteCommand = new AnnounceTransaction(this.thisStation);
				break;
			case CHANGEPUMPSTATE:
				remoteCommand = new ChangePumpState(this.thisStation);
				break;
			case CHANGERECOMMPASSWORD:
				remoteCommand = new ChangeRecommPassword(this.thisStation);
				break;
			case CHARGECREDITCARD:
				remoteCommand = new ChargeCreditCard(this.thisStation);
				break;
			case DEACTIVATEPUMP:
				remoteCommand = new DeactivatePump(this.thisStation);
				break;
			case DECOMMISSIONPUMP:
				remoteCommand = new DecommissionPump(this.thisStation);
				break;
			case DEQUEUEASSISTANCE:
				remoteCommand = new DequeueAssistance(this.thisStation);
				break;
			case NOTIFYREGISTERASSISTANCENEEDED:
				remoteCommand = new NotifyRegisterAssistanceNeeded(
						this.thisStation);
				break;
			case RECOMMISSIONPUMP:
				remoteCommand = new RecommissionPump(this.thisStation);
				break;
			case VALIDATECARD:
				remoteCommand = new ValidateCard(this.thisStation, sourceID,
						this.serverType);
				break;
			case VALIDATERECOMMPASSWORD:
				remoteCommand = new ValidateRecommPassword(this.thisStation);
				break;
		}

		if (LOGGER.isDebugEnabled())
		{
			LOGGER.debug(LogHelper.IN_COMING + "Begin to process message["
					+ command + "]. ");
		}
		executeCommand(inputMessage, remoteCommand);

		if (LOGGER.isDebugEnabled())
		{
			LOGGER.debug(LogHelper.IN_COMING + "END to process message["
					+ command + "]. ");
		}
	}

	private void executeCommand(ControlMessage inputMessage,
			RemoteCommand remoteCommand)
	{
		if (remoteCommand.executeRemoteCommand(inputMessage.getPayload()) == -1)
		{
			LOGGER.warn("payload null...");
		}
	}

	public int sendMessage(ControlMessage message, int destination)
	{
		//get a socket
		//create an ObjectOutputStream from client_socket
		//create an ObjectInputStream from client_socket
		//a destination value of -1 must cause a broadcast to all clients

		if (destination > -1)
		{
			ClientConnection client = clientConnectionsByID.get(Integer
					.valueOf(destination));

			if (client != null)
			{
				try
				{
					client.sendControlMessage(message);
				}
				catch (UicGasGeneralException e)
				{
					LOGGER.error(
							"Catch Exception while sending ControlMessage. ", e);
					return -1;
				}
			}
			else
			{
				LOGGER.error("Destination :" + destination
						+ " does not exist in " + clientConnectionsByID);
				return -1;
			}
		}
		//broadcast if destination is -1
		else
		{
			for (ClientConnection client : clientConnectionsByID.values())
			{
				try
				{
					client.sendControlMessage(message);
				}
				catch (UicGasGeneralException e)
				{
					LOGGER.error("Error broadcasting message...", e);
					return -1;
				}
			}
		}

		return 0;
	}

	public void transactCreditCard(Transaction transaction)
	{
		//perhaps write transactions that couldn't be done to a file
		System.out.println();
	}

	public void validateCardWithCompany(CreditCard card, int source)
	{

		int messageSent = 0;
		ControlMessage message = new ControlMessage(Messages.VALID, card);
		messageSent = sendMessage(message, source);

		if (messageSent != 0)
			System.out.println("Sending credit card validation failed...");
	}

	//////////////
	//////////////
	//Get Methods
	//////////////

	public int getListenPort()
	{
		return listenPort;
	}

	//	public Map<Integer, SocketChannel> getIntSockMap()
	//	{
	//		return clientSockets;
	//	}
	//
	//	public Map<SocketChannel, Integer> getSockIntMap()
	//	{
	//		return socketClients;
	//	}

	public ClientTypes getServerType()
	{
		return serverType;
	}

	public Map<Integer, ClientConnection> getClientConnectionsByID()
	{
		return clientConnectionsByID;
	}
}
